package udp;


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

import udp.message.UdpMessage;

/**
 * Handles sending a message to a UDP Server and can also receive a reply
 * @author Alex Perkins
 *
 */
public class UdpClient {

	private final int DEFAULT_TIMEOUT = 5000;
	private DatagramSocket clientSocket;
	private InetAddress ipAddress;
	private int port;
	
	public UdpClient(String host,int port) throws SocketException, UnknownHostException {
		this.port = port;
		clientSocket = new DatagramSocket();
		clientSocket.setSoTimeout(DEFAULT_TIMEOUT);
		ipAddress = InetAddress.getByName(host);
	}
	
	/**
	 * Simply sends a udp message
	 * @param message
	 */
	public void send(UdpMessage message){
		try{
			// TODO: make this reliable
			clientSocket.send(buildPacket(message));
		}
		catch(IOException e){
			System.out.println("Couldn't send message: " + e.getMessage());
			//e.printStackTrace();
		}
		finally{
			//clientSocket.close();
		}
	}
	
	/**
	 * Sends a UDP message and waits for a reply
	 * @param message
	 * @return
	 */
	public UdpMessage sendAndReceive(UdpMessage message){
		
		byte[] bytes = new byte[16384];
		DatagramPacket packet =  new DatagramPacket(bytes, bytes.length );
		//boolean sent = false;
		
		//while(!sent){
			try{
				clientSocket.send(buildPacket(message));
				clientSocket.receive(packet);
			}
			catch(IOException e){
				System.out.println("Couldn't send message: " + e.getMessage());
				e.printStackTrace();
				//sent = true;
			}
		//}
		
		UdpMessage reply = null;
		
		try{
			reply = (UdpMessage) deserialize(packet.getData());
		}
		catch(IOException e){
			System.out.println("Couldn't deserialize reply: " + e.getMessage());
		}
		catch(Exception e){
			System.out.println("Couldn't deserialize reply: " + e.getMessage());
		}
		
		return reply;
	}
	
	/**
	 * Sends a UDP message and waits for a reply
	 * @param message
	 * @return
	 * @throws SocketTimeoutException 
	 */
	public UdpMessage sendAndReceive(UdpMessage message, int timeout) throws SocketTimeoutException{
		
		byte[] bytes = new byte[16384];
		DatagramPacket packet =  new DatagramPacket(bytes, bytes.length );
		
		

			try{
				// TODO: make this reliable
				clientSocket.send(buildPacket(message));
				
				clientSocket.setSoTimeout(timeout);
				
				clientSocket.receive(packet);
			}
			catch (SocketTimeoutException e) {
				throw new SocketTimeoutException();
			}
			catch(IOException e){
				System.out.println("Couldn't send message: " + e.getMessage());
				e.printStackTrace();

			}
			finally{
				//clientSocket.close();
			}

		
		UdpMessage reply = null;
		
		try{
			reply = (UdpMessage) deserialize(packet.getData());
		}
		catch(IOException e){
			System.out.println("Couldn't deserialize reply: " + e.getMessage());
		}
		catch(Exception e){
			System.out.println("Couldn't deserialize reply: " + e.getMessage());
		}
		
		return reply;
	}
	
	
	
	
	/**
	 * Builds a packet from a UdpMessage
	 * @param message
	 * @return
	 * @throws IOException
	 */
	private DatagramPacket buildPacket(UdpMessage message) throws IOException {
		byte[] bytes = serialize(message);
		return new DatagramPacket(bytes, bytes.length, ipAddress, port);
	}
	
	/**
	 * Serializes an object to be sent over UDP
	 * @param obj
	 * @return
	 * @throws IOException
	 */
	private byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        ObjectOutputStream objectStream = new ObjectOutputStream(byteStream);
        objectStream.writeObject(obj);
        objectStream.flush();
        return byteStream.toByteArray();
    }
	
	/**
	 * Deserialize an object received over UDP
	 * @param bytes
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectStream = new ObjectInputStream(byteStream);
        return (UdpMessage) objectStream.readObject();
    }
	
}
